/*
 *  Copyright 2007 Genoworks
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License. 
 */

package com.genoworks.retriever.bigdir;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.store.SingleInstanceLockFactory;

/**
 * <p>
 * <code>VDirectory</code> stores many
 * small files into several big files
 * (eg. 2GB or 4GB) on the file system.
 * So, it can significantly reduce the
 * number of file descriptors used, thus
 * the "Too many open files" exception
 * would not occur.
 * <p>
 * Manages a set of those
 * {@link BigDiskFile}s, and
 * {@link BigFile}s.
 * 
 * @author appler
 * 
 */
public class BigDirectory extends Directory {

  private static final String DIR_NAME = "dir"; //$NON-NLS-1$

  /**
   * 2GB
   */
  private static final long DEFAULT_MAX_FILE_SIZE = Integer.MAX_VALUE;

  private ConcurrentHashMap<String, BigFile> files = new ConcurrentHashMap<String, BigFile>();

  private File baseDir = null;

  private BigDiskVolume disk = null;

  public BigDirectory( File dir ) throws IOException {
    this( dir, DEFAULT_MAX_FILE_SIZE );
  }

  public BigDirectory( File dir, long maxSize ) throws IOException {

    //
    setLockFactory( new SingleInstanceLockFactory() );

    //
    baseDir = dir;

    //
    disk = new BigDiskVolume( baseDir, maxSize );

    /*
     * 1. read previously stored
     * contents, if some exists.
     */

    // 1. directory
    File dirFile = new File( baseDir, DIR_NAME );
    if ( dirFile.exists() ) {

      //
      readDirEntries();

      // 2. data
      disk.readAllDiskFiles();
      for ( BigFile file : files.values() ) {
        disk.updateSegments( file );
      }
    }
  }

  public void close() throws IOException {

    //
    writeDirEntries();

    //
    disk.close();
  }

  /**
   * @see org.apache.lucene.store.Directory#createOutput(java.lang.String)
   */
  @SuppressWarnings( "nls" )
  public IndexOutput createOutput( String name ) throws IOException {
    synchronized( files ) {
      BigFile target = files.get( name );
      if ( target == null ) {

        /*
         * create a new VirtualFile.
         */
        target = new BigFile( name );
        files.put( name, target );

      } else {

        /*
         * empty old contents
         */
        disk.delete( target );

      }
      return new BigFileIndexOutput( target, disk, this );
    }
  }

  @SuppressWarnings( "nls" )
  public void deleteFile( String name ) throws IOException {

    //
    synchronized( files ) {

      //
      if ( files.containsKey( name ) == false ) {
        throw new FileNotFoundException( name );
      }

      //
      BigFile file = files.remove( name );
      disk.delete( file );
      writeDirEntries();
    }
  }

  @SuppressWarnings( "nls" )
  public boolean fileExists( String name ) throws IOException {
    synchronized( files ) {
      return files.containsKey( name );
    }
  }

  @SuppressWarnings( "nls" )
  public long fileLength( String name ) throws IOException {
    synchronized( files ) {

      //
      BigFile file = files.get( name );
      if ( file == null ) {
        throw new FileNotFoundException( name );
      }

      //
      return file.getLength();
    }
  }

  @SuppressWarnings( "nls" )
  public long fileModified( String name ) throws IOException {
    synchronized( files ) {

      //
      BigFile file = files.get( name );
      if ( file == null ) {
        throw new FileNotFoundException( name );
      }

      //
      return file.getLastModified();
    }
  }

  @SuppressWarnings( "nls" )
  public String[] list() throws IOException {
    synchronized( files ) {
      String[] result = new String[ files.size() ];
      int i = 0;
      for ( String name : files.keySet() ) {
        result[ i++ ] = name;
      }
      return result;
    }
  }

  @SuppressWarnings( "nls" )
  public IndexInput openInput( String name ) throws IOException {
    synchronized( files ) {
      BigFile file = files.get( name );
      if ( file == null ) {
        throw new FileNotFoundException( name );
      }
      return new BigFileIndexInput( file, disk );
    }
  }

  @SuppressWarnings( "nls" )
  public void renameFile( String from, String to ) throws IOException {
    synchronized( files ) {
      BigFile file = files.remove( from );
      if ( file != null ) {
        files.put( to, file );
      }
    }
  }

  @SuppressWarnings( "nls" )
  public void touchFile( String name ) throws IOException {
    synchronized( files ) {

      //
      BigFile file = files.get( name );
      if ( file == null ) {
        throw new FileNotFoundException( name );
      }

      //
      file.setLastModified( System.currentTimeMillis() );
    }
  }

  /**
   * Called when
   * <ol>
   * <li>VDirectory closes,</li>
   * <li>or each VFileIndexOutput
   * closes.</li>
   * </ol>
   * 
   * @throws IOException
   */
  void writeDirEntries() throws IOException {

    //
    final File dirFile = new File( baseDir, DIR_NAME );

    // 1. compose
    StringBuilder str = new StringBuilder();
    boolean isFirst = true;
    for ( BigFile file : files.values() ) {
      if ( isFirst ) {
        isFirst = false;
      } else {
        str.append( '|' );
      }
      str.append( file.toDirString() );
    }

    // 2. write to file
    PrintWriter out = new PrintWriter( new FileWriter( dirFile ) );
    try {
      out.print( str.toString() );
      out.flush();
    } finally {
      out.close();
    }
  }

  private void readDirEntries() throws IOException {

    //
    final File dirFile = new File( baseDir, DIR_NAME );

    // 1. read from file
    BufferedReader reader = new BufferedReader( new FileReader( dirFile ) );
    StringBuilder dirEntriesStr = new StringBuilder();
    try {
      char[] charBuffer = new char[ 1024 ];
      int charLength = 0;
      while ( ( charLength = reader.read( charBuffer ) ) >= 0 ) {
        dirEntriesStr.append( charBuffer, 0, charLength );
      }
    } finally {
      reader.close();
    }

    // 2. tokenize
    StringTokenizer st = new StringTokenizer( dirEntriesStr.toString(), "|" ); //$NON-NLS-1$
    while ( st.hasMoreTokens() ) {
      BigFile bf = BigFile.fromDirString( st.nextToken() );
      files.put( bf.getName(), bf );
    }
  }

  public static String arrayToString( String[] arr ) {
    StringBuilder result = new StringBuilder();
    boolean isFirst = true;
    for ( String a : arr ) {
      if ( isFirst ) {
        isFirst = false;
      } else {
        result.append( ',' );
      }
      result.append( a );
    }
    return result.toString();
  }

  public static String byteToString( byte[] buffer, int offset, int length ) {
    StringBuilder result = new StringBuilder();
    boolean isFirst = true;
    for ( int i = 0; i < length; i++ ) {
      byte b = buffer[ i + offset ];
      if ( isFirst ) {
        isFirst = false;
      } else {
        result.append( ',' );
      }
      result.append( Integer.toString( b ) );
    }
    return result.toString();
  }

  public String toString() {
    String path = null;
    if ( baseDir != null ) {
      try {
        path = baseDir.getCanonicalPath();
      } catch( IOException e ) {
        path = baseDir.getAbsolutePath();
      }
    }
    return "BigDirectory{dir=" + path + ",files=" + files.keySet() + "}";
  }
}
